import {WebGLUniforms} from './WebGLUniforms.js';
import {WebGLShader} from './WebGLShader.js';
import {ShaderChunk} from '../shaders/ShaderChunk.js';
import {
  NoToneMapping,
  AddOperation,
  MixOperation,
  MultiplyOperation,
  CubeRefractionMapping,
  CubeUVRefractionMapping,
  CubeUVReflectionMapping,
  CubeReflectionMapping,
  PCFSoftShadowMap,
  PCFShadowMap,
  VSMShadowMap,
  ACESFilmicToneMapping,
  CineonToneMapping,
  CustomToneMapping,
  ReinhardToneMapping,
  LinearToneMapping,
  GammaEncoding,
  RGBDEncoding,
  RGBM16Encoding,
  RGBM7Encoding,
  RGBEEncoding,
  sRGBEncoding,
  LinearEncoding,
  LogLuvEncoding,
  GLSL3
} from '../../constants.js';

let programIdCount = 0;

function addLineNumbers(string) {

  const lines = string.split('\n');

  for (let i = 0; i < lines.length; i++) {

    lines[i] = (i + 1) + ': ' + lines[i];

  }

  return lines.join('\n');

}

function getEncodingComponents(encoding) {

  switch (encoding) {

    case LinearEncoding:
      return ['Linear', '( value )'];
    case sRGBEncoding:
      return ['sRGB', '( value )'];
    case RGBEEncoding:
      return ['RGBE', '( value )'];
    case RGBM7Encoding:
      return ['RGBM', '( value, 7.0 )'];
    case RGBM16Encoding:
      return ['RGBM', '( value, 16.0 )'];
    case RGBDEncoding:
      return ['RGBD', '( value, 256.0 )'];
    case GammaEncoding:
      return ['Gamma', '( value, float( GAMMA_FACTOR ) )'];
    case LogLuvEncoding:
      return ['LogLuv', '( value )'];
    default:
      console.warn('THREE.WebGLProgram: Unsupported encoding:', encoding);
      return ['Linear', '( value )'];

  }

}

function getShaderErrors(gl, shader, type) {

  const status = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
  const log = gl.getShaderInfoLog(shader).trim();

  if (status && log === '') return '';

  // --enable-privileged-webgl-extension
  // console.log( '**' + type + '**', gl.getExtension( 'WEBGL_debug_shaders' ).getTranslatedShaderSource( shader ) );

  const source = gl.getShaderSource(shader);

  return 'THREE.WebGLShader: gl.getShaderInfoLog() ' + type + '\n' + log + addLineNumbers(source);

}

function getTexelDecodingFunction(functionName, encoding) {

  const components = getEncodingComponents(encoding);
  return 'vec4 ' + functionName + '( vec4 value ) { return ' + components[0] + 'ToLinear' + components[1] + '; }';

}

function getTexelEncodingFunction(functionName, encoding) {

  const components = getEncodingComponents(encoding);
  return 'vec4 ' + functionName + '( vec4 value ) { return LinearTo' + components[0] + components[1] + '; }';

}

function getToneMappingFunction(functionName, toneMapping) {

  let toneMappingName;

  switch (toneMapping) {

    case LinearToneMapping:
      toneMappingName = 'Linear';
      break;

    case ReinhardToneMapping:
      toneMappingName = 'Reinhard';
      break;

    case CineonToneMapping:
      toneMappingName = 'OptimizedCineon';
      break;

    case ACESFilmicToneMapping:
      toneMappingName = 'ACESFilmic';
      break;

    case CustomToneMapping:
      toneMappingName = 'Custom';
      break;

    default:
      console.warn('THREE.WebGLProgram: Unsupported toneMapping:', toneMapping);
      toneMappingName = 'Linear';

  }

  return 'vec3 ' + functionName + '( vec3 color ) { return ' + toneMappingName + 'ToneMapping( color ); }';

}

function generateExtensions(parameters) {

  const chunks = [
    (parameters.extensionDerivatives || parameters.envMapCubeUV || parameters.bumpMap || parameters.tangentSpaceNormalMap || parameters.clearcoatNormalMap || parameters.flatShading || parameters.shaderID === 'physical') ? '#extension GL_OES_standard_derivatives : enable' : '',
    (parameters.extensionFragDepth || parameters.logarithmicDepthBuffer) && parameters.rendererExtensionFragDepth ? '#extension GL_EXT_frag_depth : enable' : '',
    (parameters.extensionDrawBuffers && parameters.rendererExtensionDrawBuffers) ? '#extension GL_EXT_draw_buffers : require' : '',
    (parameters.extensionShaderTextureLOD || parameters.envMap) && parameters.rendererExtensionShaderTextureLod ? '#extension GL_EXT_shader_texture_lod : enable' : ''
  ];

  return chunks.filter(filterEmptyLine).join('\n');

}

function generateDefines(defines) {

  const chunks = [];

  for (const name in defines) {

    const value = defines[name];

    if (value === false) continue;

    chunks.push('#define ' + name + ' ' + value);

  }

  return chunks.join('\n');

}

function fetchAttributeLocations(gl, program) {

  const attributes = {};

  const n = gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES);

  for (let i = 0; i < n; i++) {

    const info = gl.getActiveAttrib(program, i);
    const name = info.name;

    // console.log( 'THREE.WebGLProgram: ACTIVE VERTEX ATTRIBUTE:', name, i );

    attributes[name] = gl.getAttribLocation(program, name);

  }

  return attributes;

}

function filterEmptyLine(string) {

  return string !== '';

}

function replaceLightNums(string, parameters) {

  return string
    .replace(/NUM_DIR_LIGHTS/g, parameters.numDirLights)
    .replace(/NUM_SPOT_LIGHTS/g, parameters.numSpotLights)
    .replace(/NUM_RECT_AREA_LIGHTS/g, parameters.numRectAreaLights)
    .replace(/NUM_POINT_LIGHTS/g, parameters.numPointLights)
    .replace(/NUM_HEMI_LIGHTS/g, parameters.numHemiLights)
    .replace(/NUM_DIR_LIGHT_SHADOWS/g, parameters.numDirLightShadows)
    .replace(/NUM_SPOT_LIGHT_SHADOWS/g, parameters.numSpotLightShadows)
    .replace(/NUM_POINT_LIGHT_SHADOWS/g, parameters.numPointLightShadows);

}

function replaceClippingPlaneNums(string, parameters) {

  return string
    .replace(/NUM_CLIPPING_PLANES/g, parameters.numClippingPlanes)
    .replace(/UNION_CLIPPING_PLANES/g, (parameters.numClippingPlanes - parameters.numClipIntersection));

}

// Resolve Includes

const includePattern = /^[ \t]*#include +<([\w\d./]+)>/gm;

function resolveIncludes(string) {

  return string.replace(includePattern, includeReplacer);

}

function includeReplacer(match, include) {

  const string = ShaderChunk[include];

  if (string === undefined) {

    throw new Error('Can not resolve #include <' + include + '>');

  }

  return resolveIncludes(string);

}

// Unroll Loops

const deprecatedUnrollLoopPattern = /#pragma unroll_loop[\s]+?for \( int i \= (\d+)\; i < (\d+)\; i \+\+ \) \{([\s\S]+?)(?=\})\}/g;
const unrollLoopPattern = /#pragma unroll_loop_start\s+for\s*\(\s*int\s+i\s*=\s*(\d+)\s*;\s*i\s*<\s*(\d+)\s*;\s*i\s*\+\+\s*\)\s*{([\s\S]+?)}\s+#pragma unroll_loop_end/g;

function unrollLoops(string) {

  return string
    .replace(unrollLoopPattern, loopReplacer)
    .replace(deprecatedUnrollLoopPattern, deprecatedLoopReplacer);

}

function deprecatedLoopReplacer(match, start, end, snippet) {

  console.warn('WebGLProgram: #pragma unroll_loop shader syntax is deprecated. Please use #pragma unroll_loop_start syntax instead.');
  return loopReplacer(match, start, end, snippet);

}

function loopReplacer(match, start, end, snippet) {

  let string = '';

  for (let i = parseInt(start); i < parseInt(end); i++) {

    string += snippet
      .replace(/\[\s*i\s*\]/g, '[ ' + i + ' ]')
      .replace(/UNROLLED_LOOP_INDEX/g, i);

  }

  return string;

}

//

function generatePrecision(parameters) {

  let precisionstring = "precision " + parameters.precision + " float;\nprecision " + parameters.precision + " int;";

  if (parameters.precision === "highp") {

    precisionstring += "\n#define HIGH_PRECISION";

  } else if (parameters.precision === "mediump") {

    precisionstring += "\n#define MEDIUM_PRECISION";

  } else if (parameters.precision === "lowp") {

    precisionstring += "\n#define LOW_PRECISION";

  }

  return precisionstring;

}

function generateShadowMapTypeDefine(parameters) {

  let shadowMapTypeDefine = 'SHADOWMAP_TYPE_BASIC';

  if (parameters.shadowMapType === PCFShadowMap) {

    shadowMapTypeDefine = 'SHADOWMAP_TYPE_PCF';

  } else if (parameters.shadowMapType === PCFSoftShadowMap) {

    shadowMapTypeDefine = 'SHADOWMAP_TYPE_PCF_SOFT';

  } else if (parameters.shadowMapType === VSMShadowMap) {

    shadowMapTypeDefine = 'SHADOWMAP_TYPE_VSM';

  }

  return shadowMapTypeDefine;

}

function generateEnvMapTypeDefine(parameters) {

  let envMapTypeDefine = 'ENVMAP_TYPE_CUBE';

  if (parameters.envMap) {

    switch (parameters.envMapMode) {

      case CubeReflectionMapping:
      case CubeRefractionMapping:
        envMapTypeDefine = 'ENVMAP_TYPE_CUBE';
        break;

      case CubeUVReflectionMapping:
      case CubeUVRefractionMapping:
        envMapTypeDefine = 'ENVMAP_TYPE_CUBE_UV';
        break;

    }

  }

  return envMapTypeDefine;

}

function generateEnvMapModeDefine(parameters) {

  let envMapModeDefine = 'ENVMAP_MODE_REFLECTION';

  if (parameters.envMap) {

    switch (parameters.envMapMode) {

      case CubeRefractionMapping:
      case CubeUVRefractionMapping:

        envMapModeDefine = 'ENVMAP_MODE_REFRACTION';
        break;

    }

  }

  return envMapModeDefine;

}

function generateEnvMapBlendingDefine(parameters) {

  let envMapBlendingDefine = 'ENVMAP_BLENDING_NONE';

  if (parameters.envMap) {

    switch (parameters.combine) {

      case MultiplyOperation:
        envMapBlendingDefine = 'ENVMAP_BLENDING_MULTIPLY';
        break;

      case MixOperation:
        envMapBlendingDefine = 'ENVMAP_BLENDING_MIX';
        break;

      case AddOperation:
        envMapBlendingDefine = 'ENVMAP_BLENDING_ADD';
        break;

    }

  }

  return envMapBlendingDefine;

}

function WebGLProgram(renderer, cacheKey, parameters, bindingStates) {

  const gl = renderer.getContext();

  const defines = parameters.defines;

  let vertexShader = parameters.vertexShader;
  let fragmentShader = parameters.fragmentShader;

  const shadowMapTypeDefine = generateShadowMapTypeDefine(parameters);
  const envMapTypeDefine = generateEnvMapTypeDefine(parameters);
  const envMapModeDefine = generateEnvMapModeDefine(parameters);
  const envMapBlendingDefine = generateEnvMapBlendingDefine(parameters);


  const gammaFactorDefine = (renderer.gammaFactor > 0) ? renderer.gammaFactor : 1.0;

  const customExtensions = parameters.isWebGL2 ? '' : generateExtensions(parameters);

  const customDefines = generateDefines(defines);

  const program = gl.createProgram();

  let prefixVertex, prefixFragment;
  let versionString = parameters.glslVersion ? '#version ' + parameters.glslVersion + "\n" : '';

  if (parameters.isRawShaderMaterial) {

    prefixVertex = [

      customDefines

    ].filter(filterEmptyLine).join('\n');

    if (prefixVertex.length > 0) {

      prefixVertex += '\n';

    }

    prefixFragment = [

      customExtensions,
      customDefines

    ].filter(filterEmptyLine).join('\n');

    if (prefixFragment.length > 0) {

      prefixFragment += '\n';

    }

  } else {

    prefixVertex = [

      generatePrecision(parameters),

      '#define SHADER_NAME ' + parameters.shaderName,

      customDefines,

      parameters.instancing ? '#define USE_INSTANCING' : '',
      parameters.instancingColor ? '#define USE_INSTANCING_COLOR' : '',

      parameters.supportsVertexTextures ? '#define VERTEX_TEXTURES' : '',

      '#define GAMMA_FACTOR ' + gammaFactorDefine,

      '#define MAX_BONES ' + parameters.maxBones,
      (parameters.useFog && parameters.fog) ? '#define USE_FOG' : '',
      (parameters.useFog && parameters.fogExp2) ? '#define FOG_EXP2' : '',

      parameters.map ? '#define USE_MAP' : '',
      parameters.envMap ? '#define USE_ENVMAP' : '',
      parameters.envMap ? '#define ' + envMapModeDefine : '',
      parameters.lightMap ? '#define USE_LIGHTMAP' : '',
      parameters.aoMap ? '#define USE_AOMAP' : '',
      parameters.emissiveMap ? '#define USE_EMISSIVEMAP' : '',
      parameters.bumpMap ? '#define USE_BUMPMAP' : '',
      parameters.normalMap ? '#define USE_NORMALMAP' : '',
      (parameters.normalMap && parameters.objectSpaceNormalMap) ? '#define OBJECTSPACE_NORMALMAP' : '',
      (parameters.normalMap && parameters.tangentSpaceNormalMap) ? '#define TANGENTSPACE_NORMALMAP' : '',

      parameters.clearcoatMap ? '#define USE_CLEARCOATMAP' : '',
      parameters.clearcoatRoughnessMap ? '#define USE_CLEARCOAT_ROUGHNESSMAP' : '',
      parameters.clearcoatNormalMap ? '#define USE_CLEARCOAT_NORMALMAP' : '',
      parameters.displacementMap && parameters.supportsVertexTextures ? '#define USE_DISPLACEMENTMAP' : '',
      parameters.specularMap ? '#define USE_SPECULARMAP' : '',
      parameters.roughnessMap ? '#define USE_ROUGHNESSMAP' : '',
      parameters.metalnessMap ? '#define USE_METALNESSMAP' : '',
      parameters.alphaMap ? '#define USE_ALPHAMAP' : '',
      parameters.transmissionMap ? '#define USE_TRANSMISSIONMAP' : '',

      parameters.vertexTangents ? '#define USE_TANGENT' : '',
      parameters.vertexColors ? '#define USE_COLOR' : '',
      parameters.vertexUvs ? '#define USE_UV' : '',
      parameters.uvsVertexOnly ? '#define UVS_VERTEX_ONLY' : '',

      parameters.flatShading ? '#define FLAT_SHADED' : '',

      parameters.skinning ? '#define USE_SKINNING' : '',
      parameters.useVertexTexture ? '#define BONE_TEXTURE' : '',

      parameters.morphTargets ? '#define USE_MORPHTARGETS' : '',
      parameters.morphNormals && parameters.flatShading === false ? '#define USE_MORPHNORMALS' : '',
      parameters.doubleSided ? '#define DOUBLE_SIDED' : '',
      parameters.flipSided ? '#define FLIP_SIDED' : '',

      parameters.shadowMapEnabled ? '#define USE_SHADOWMAP' : '',
      parameters.shadowMapEnabled ? '#define ' + shadowMapTypeDefine : '',

      parameters.sizeAttenuation ? '#define USE_SIZEATTENUATION' : '',

      parameters.logarithmicDepthBuffer ? '#define USE_LOGDEPTHBUF' : '',
      (parameters.logarithmicDepthBuffer && parameters.rendererExtensionFragDepth) ? '#define USE_LOGDEPTHBUF_EXT' : '',

      'uniform mat4 modelMatrix;',
      'uniform mat4 modelViewMatrix;',
      'uniform mat4 projectionMatrix;',
      'uniform mat4 viewMatrix;',
      'uniform mat3 normalMatrix;',
      'uniform vec3 cameraPosition;',
      'uniform bool isOrthographic;',

      '#ifdef USE_INSTANCING',

      '	attribute mat4 instanceMatrix;',

      '#endif',

      '#ifdef USE_INSTANCING_COLOR',

      '	attribute vec3 instanceColor;',

      '#endif',

      'attribute vec3 position;',
      'attribute vec3 normal;',
      'attribute vec2 uv;',

      '#ifdef USE_TANGENT',

      '	attribute vec4 tangent;',

      '#endif',

      '#ifdef USE_COLOR',

      '	attribute vec3 color;',

      '#endif',

      '#ifdef USE_MORPHTARGETS',

      '	attribute vec3 morphTarget0;',
      '	attribute vec3 morphTarget1;',
      '	attribute vec3 morphTarget2;',
      '	attribute vec3 morphTarget3;',

      '	#ifdef USE_MORPHNORMALS',

      '		attribute vec3 morphNormal0;',
      '		attribute vec3 morphNormal1;',
      '		attribute vec3 morphNormal2;',
      '		attribute vec3 morphNormal3;',

      '	#else',

      '		attribute vec3 morphTarget4;',
      '		attribute vec3 morphTarget5;',
      '		attribute vec3 morphTarget6;',
      '		attribute vec3 morphTarget7;',

      '	#endif',

      '#endif',

      '#ifdef USE_SKINNING',

      '	attribute vec4 skinIndex;',
      '	attribute vec4 skinWeight;',

      '#endif',

      '\n'

    ].filter(filterEmptyLine).join('\n');

    prefixFragment = [

      customExtensions,

      generatePrecision(parameters),

      '#define SHADER_NAME ' + parameters.shaderName,

      customDefines,

      parameters.alphaTest ? '#define ALPHATEST ' + parameters.alphaTest + (parameters.alphaTest % 1 ? '' : '.0') : '', // add '.0' if integer

      '#define GAMMA_FACTOR ' + gammaFactorDefine,

      (parameters.useFog && parameters.fog) ? '#define USE_FOG' : '',
      (parameters.useFog && parameters.fogExp2) ? '#define FOG_EXP2' : '',

      parameters.map ? '#define USE_MAP' : '',
      parameters.matcap ? '#define USE_MATCAP' : '',
      parameters.envMap ? '#define USE_ENVMAP' : '',
      parameters.envMap ? '#define ' + envMapTypeDefine : '',
      parameters.envMap ? '#define ' + envMapModeDefine : '',
      parameters.envMap ? '#define ' + envMapBlendingDefine : '',
      parameters.lightMap ? '#define USE_LIGHTMAP' : '',
      parameters.aoMap ? '#define USE_AOMAP' : '',
      parameters.emissiveMap ? '#define USE_EMISSIVEMAP' : '',
      parameters.bumpMap ? '#define USE_BUMPMAP' : '',
      parameters.normalMap ? '#define USE_NORMALMAP' : '',
      (parameters.normalMap && parameters.objectSpaceNormalMap) ? '#define OBJECTSPACE_NORMALMAP' : '',
      (parameters.normalMap && parameters.tangentSpaceNormalMap) ? '#define TANGENTSPACE_NORMALMAP' : '',
      parameters.clearcoatMap ? '#define USE_CLEARCOATMAP' : '',
      parameters.clearcoatRoughnessMap ? '#define USE_CLEARCOAT_ROUGHNESSMAP' : '',
      parameters.clearcoatNormalMap ? '#define USE_CLEARCOAT_NORMALMAP' : '',
      parameters.specularMap ? '#define USE_SPECULARMAP' : '',
      parameters.roughnessMap ? '#define USE_ROUGHNESSMAP' : '',
      parameters.metalnessMap ? '#define USE_METALNESSMAP' : '',
      parameters.alphaMap ? '#define USE_ALPHAMAP' : '',

      parameters.sheen ? '#define USE_SHEEN' : '',
      parameters.transmissionMap ? '#define USE_TRANSMISSIONMAP' : '',

      parameters.vertexTangents ? '#define USE_TANGENT' : '',
      parameters.vertexColors || parameters.instancingColor ? '#define USE_COLOR' : '',
      parameters.vertexUvs ? '#define USE_UV' : '',
      parameters.uvsVertexOnly ? '#define UVS_VERTEX_ONLY' : '',

      parameters.gradientMap ? '#define USE_GRADIENTMAP' : '',

      parameters.flatShading ? '#define FLAT_SHADED' : '',

      parameters.doubleSided ? '#define DOUBLE_SIDED' : '',
      parameters.flipSided ? '#define FLIP_SIDED' : '',

      parameters.shadowMapEnabled ? '#define USE_SHADOWMAP' : '',
      parameters.shadowMapEnabled ? '#define ' + shadowMapTypeDefine : '',

      parameters.premultipliedAlpha ? '#define PREMULTIPLIED_ALPHA' : '',

      parameters.physicallyCorrectLights ? '#define PHYSICALLY_CORRECT_LIGHTS' : '',

      parameters.logarithmicDepthBuffer ? '#define USE_LOGDEPTHBUF' : '',
      (parameters.logarithmicDepthBuffer && parameters.rendererExtensionFragDepth) ? '#define USE_LOGDEPTHBUF_EXT' : '',

      ((parameters.extensionShaderTextureLOD || parameters.envMap) && parameters.rendererExtensionShaderTextureLod) ? '#define TEXTURE_LOD_EXT' : '',

      'uniform mat4 viewMatrix;',
      'uniform vec3 cameraPosition;',
      'uniform bool isOrthographic;',

      (parameters.toneMapping !== NoToneMapping) ? '#define TONE_MAPPING' : '',
      (parameters.toneMapping !== NoToneMapping) ? ShaderChunk['tonemapping_pars_fragment'] : '', // this code is required here because it is used by the toneMapping() function defined below
      (parameters.toneMapping !== NoToneMapping) ? getToneMappingFunction('toneMapping', parameters.toneMapping) : '',

      parameters.dithering ? '#define DITHERING' : '',

      ShaderChunk['encodings_pars_fragment'], // this code is required here because it is used by the various encoding/decoding function defined below
      parameters.map ? getTexelDecodingFunction('mapTexelToLinear', parameters.mapEncoding) : '',
      parameters.matcap ? getTexelDecodingFunction('matcapTexelToLinear', parameters.matcapEncoding) : '',
      parameters.envMap ? getTexelDecodingFunction('envMapTexelToLinear', parameters.envMapEncoding) : '',
      parameters.emissiveMap ? getTexelDecodingFunction('emissiveMapTexelToLinear', parameters.emissiveMapEncoding) : '',
      parameters.lightMap ? getTexelDecodingFunction('lightMapTexelToLinear', parameters.lightMapEncoding) : '',
      getTexelEncodingFunction('linearToOutputTexel', parameters.outputEncoding),

      parameters.depthPacking ? '#define DEPTH_PACKING ' + parameters.depthPacking : '',

      '\n'

    ].filter(filterEmptyLine).join('\n');

  }

  vertexShader = resolveIncludes(vertexShader);
  vertexShader = replaceLightNums(vertexShader, parameters);
  vertexShader = replaceClippingPlaneNums(vertexShader, parameters);

  fragmentShader = resolveIncludes(fragmentShader);
  fragmentShader = replaceLightNums(fragmentShader, parameters);
  fragmentShader = replaceClippingPlaneNums(fragmentShader, parameters);

  vertexShader = unrollLoops(vertexShader);
  fragmentShader = unrollLoops(fragmentShader);

  if (parameters.isWebGL2 && parameters.isRawShaderMaterial !== true) {

    // GLSL 3.0 conversion for built-in materials and ShaderMaterial

    versionString = '#version 300 es\n';

    prefixVertex = [
      '#define attribute in',
      '#define varying out',
      '#define texture2D texture'
    ].join('\n') + '\n' + prefixVertex;

    prefixFragment = [
      '#define varying in',
      (parameters.glslVersion === GLSL3) ? '' : 'out highp vec4 pc_fragColor;',
      (parameters.glslVersion === GLSL3) ? '' : '#define gl_FragColor pc_fragColor',
      '#define gl_FragDepthEXT gl_FragDepth',
      '#define texture2D texture',
      '#define textureCube texture',
      '#define texture2DProj textureProj',
      '#define texture2DLodEXT textureLod',
      '#define texture2DProjLodEXT textureProjLod',
      '#define textureCubeLodEXT textureLod',
      '#define texture2DGradEXT textureGrad',
      '#define texture2DProjGradEXT textureProjGrad',
      '#define textureCubeGradEXT textureGrad'
    ].join('\n') + '\n' + prefixFragment;

  }

  const vertexGlsl = versionString + prefixVertex + vertexShader;
  const fragmentGlsl = versionString + prefixFragment + fragmentShader;

  // console.log( '*VERTEX*', vertexGlsl );
  // console.log( '*FRAGMENT*', fragmentGlsl );

  const glVertexShader = WebGLShader(gl, gl.VERTEX_SHADER, vertexGlsl);
  const glFragmentShader = WebGLShader(gl, gl.FRAGMENT_SHADER, fragmentGlsl);

  gl.attachShader(program, glVertexShader);
  gl.attachShader(program, glFragmentShader);

  // Force a particular attribute to index 0.

  if (parameters.index0AttributeName !== undefined) {

    gl.bindAttribLocation(program, 0, parameters.index0AttributeName);

  } else if (parameters.morphTargets === true) {

    // programs with morphTargets displace position out of attribute 0
    gl.bindAttribLocation(program, 0, 'position');

  }

  gl.linkProgram(program);

  // check for link errors
  if (renderer.debug.checkShaderErrors) {

    const programLog = gl.getProgramInfoLog(program).trim();
    const vertexLog = gl.getShaderInfoLog(glVertexShader).trim();
    const fragmentLog = gl.getShaderInfoLog(glFragmentShader).trim();

    let runnable = true;
    let haveDiagnostics = true;

    if (gl.getProgramParameter(program, gl.LINK_STATUS) === false) {

      runnable = false;

      const vertexErrors = getShaderErrors(gl, glVertexShader, 'vertex');
      const fragmentErrors = getShaderErrors(gl, glFragmentShader, 'fragment');

      console.error('THREE.WebGLProgram: shader error: ', gl.getError(), 'gl.VALIDATE_STATUS', gl.getProgramParameter(program, gl.VALIDATE_STATUS), 'gl.getProgramInfoLog', programLog, vertexErrors, fragmentErrors);

    } else if (programLog !== '') {

      console.warn('THREE.WebGLProgram: gl.getProgramInfoLog()', programLog);

    } else if (vertexLog === '' || fragmentLog === '') {

      haveDiagnostics = false;

    }

    if (haveDiagnostics) {

      this.diagnostics = {

        runnable: runnable,

        programLog: programLog,

        vertexShader: {

          log: vertexLog,
          prefix: prefixVertex

        },

        fragmentShader: {

          log: fragmentLog,
          prefix: prefixFragment

        }

      };

    }

  }

  // Clean up

  // Crashes in iOS9 and iOS10. #18402
  // gl.detachShader( program, glVertexShader );
  // gl.detachShader( program, glFragmentShader );

  gl.deleteShader(glVertexShader);
  gl.deleteShader(glFragmentShader);

  // set up caching for uniform locations

  let cachedUniforms;

  this.getUniforms = function () {

    if (cachedUniforms === undefined) {

      cachedUniforms = new WebGLUniforms(gl, program);

    }

    return cachedUniforms;

  };

  // set up caching for attribute locations

  let cachedAttributes;

  this.getAttributes = function () {

    if (cachedAttributes === undefined) {

      cachedAttributes = fetchAttributeLocations(gl, program);

    }

    return cachedAttributes;

  };

  // free resource

  this.destroy = function () {

    bindingStates.releaseStatesOfProgram(this);

    gl.deleteProgram(program);
    this.program = undefined;

  };

  //

  this.name = parameters.shaderName;
  this.id = programIdCount++;
  this.cacheKey = cacheKey;
  this.usedTimes = 1;
  this.program = program;
  this.vertexShader = glVertexShader;
  this.fragmentShader = glFragmentShader;

  return this;

}

export {WebGLProgram};
